//-----------------------------------------------------------------------
// <copyright file="ControlService.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
// </copyright>
// <author>Mike Fourie</author>
// <email>v-mifour</email>
// <date>2008-02-11</date>
// <summary>Controls a Win32 Service on the local computer.</summary>
//-----------------------------------------------------------------------
namespace Microsoft.Sdc.Tasks.ServiceProcess
{
    using System;
    using System.IO;
    using System.Diagnostics;
    using System.Globalization;
    using System.Threading;
    using Microsoft.Win32;
    using System.ServiceProcess;
    using Microsoft.Build.Framework;
    using System.Management;

    /// <summary>
    /// Controls a Win32 Service on the local computer
    /// </summary>
    /// <remarks>
    /// <code><![CDATA[
    /// <ControlService Action="action" ServiceName="servicename"/>
    /// 
    /// <ControlService Action="Uninstall" ServiceName="servicename" ServiceExePath="serviceexepath"/>
    /// 
    /// <ControlService Action="Install" ServiceName="servicename" User="user" Password="password" ServiceExePath="serviceexepath"/>
    /// 
    /// ]]></code>
    /// <para>where:</para>
    /// <para><i>action (Required)</i></para>
    /// <para>The action to perform on the service. Supported actions are: Install, Uninstall, Start, Stop, Disabled, Manual, Automatic</para>
    /// <para><i>servicename (Required)</i></para>
    /// <para>The name of the local Win32 service to control</para>
    /// <para><i>serviceexepath</i></para>
    /// <para>The path to the service executable</para>
    /// <para><i>user</i></para>
    /// <para>The name of the user to run the service under</para>
    /// <para><i>password</i></para>
    /// <para>The password of the user to run the service under</para>
    /// </remarks>
    /// <example>
    /// <code><![CDATA[
    /// 
    /// <ControlService Action="Start" ServiceName="W3SVC"/>
    /// 
    /// <ControlService Action="Disabled" ServiceName="aspnet_state"/>
    /// 
    /// ]]></code>    
    /// </example>
    public class ControlService : TaskBase
    {
        // We will attempt the action every 1 seconds.
        private readonly int pauseMilliSeconds = 1000;

        // We will attempt the action for a maximum of 2 minutes (NT defined service timeout).
        private readonly int maxCount = 120;
        private string path;
        private string action;
        private string serviceName;
        private string serviceExePath;
        private string user;
        private string password;
        private string machineName;
        private bool serviceExists;

        /// <summary>
        /// Gets or sets the password.
        /// </summary>
        /// <value>The password.</value>
        public string Password
        {
            get { return this.password; }
            set { this.password = value; }
        }

        /// <summary>
        /// Gets or sets the service exists.
        /// </summary>
        /// <value>The service exists.</value>
        public bool ServiceExists
        {
            get { return this.serviceExists; }
            set { this.serviceExists = value; }
        }

        /// <summary>
        /// Gets or sets the name of the machine.
        /// </summary>
        /// <value>The name of the machine.</value>
        public string MachineName
        {
            get { return (this.machineName ?? Environment.MachineName); }
            set { this.machineName = value; }
        }

        /// <summary>
        /// Gets or sets the user.
        /// </summary>
        /// <value>The user.</value>
        public string User
        {
            get { return this.user; }
            set { this.user = value; }
        }

        /// <summary>
        /// Gets or sets the name of Service.
        /// </summary>
        /// <value>The name of Service.</value>
        [Required]
        public string ServiceName
        {
            get { return this.serviceName; }
            set { this.serviceName = value; }
        }

        /// <summary>
        /// Gets or sets Service exe path.
        /// </summary>
        /// <value>Service exe path.</value>
        public string ServiceExePath
        {
            get { return this.serviceExePath; }
            set { this.serviceExePath = value; }
        }

        /// <summary>
        /// Gets or sets the action.
        /// </summary>
        /// <value>The action.</value>
        [Required]
        public string Action
        {
            get { return this.action; }
            set { this.action = value; }
        }

        /// <summary>
        /// Gets or sets the path.
        /// </summary>
        /// <value>The path.</value>
        public string Path
        {
            get { return this.path; }
            set { this.path = value; }
        }

        /// <summary>
        /// Performs the action of this task.
        /// </summary>
        protected override void InternalExecute()
        {
            #region Execute code
            try
            {
                // see if Service exists
                if (this.ServiceDoesExist() == false && this.Action != "Install")
                {
                    Log.LogMessage(MessageImportance.High, string.Format("Service does not exist: {0}", this.ServiceName));
                    return;
                }

                switch (this.Action)
                {
                    case "Install":
                        this.InstallService();
                        break;
                    case "Uninstall":
                        this.UninstallService();
                        break;
                    case "Stop":
                        this.StopService();
                        break;
                    case "Start":
                        this.StartService();
                        break;
                    case "Disabled":
                        this.SetStartupType("Disabled");
                        break;
                    case "Manual":
                        this.SetStartupType("Manual");
                        break;
                    case "Automatic":
                        this.SetStartupType("Automatic");
                        break;
                    case "Exists":
                        this.CheckExists();
                        break;
                    default:
                        Log.LogError(string.Format("Invalid Action Passed to ControlService: {0}. Actions supported are Automatic, Disabled, Manual, Install, Start, Stop, Exists and Uninstall", this.Action));
                        return;
                }
            }
            catch (Exception ex)
            {
                Log.LogErrorFromException(ex, true);
            }
            #endregion
        }

        /// <summary>
        /// Gets the install util path.
        /// </summary>
        /// <returns>a string value</returns>
        private static string GetInstallUtilPath()
        {
            RegistryKey runtimeKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\.NETFramework");
            string pathToFramework = Convert.ToString(runtimeKey.GetValue("InstallRoot"), CultureInfo.InvariantCulture);
            runtimeKey.Close();
            return System.IO.Path.Combine(System.IO.Path.Combine(pathToFramework, "v2.0.50727"), "installutil.exe");
        }

        /// <summary>
        /// Checks the service exists.
        /// </summary>
        private void CheckExists()
        {
            if (this.ServiceDoesExist())
            {
                this.ServiceExists = true;
                Log.LogMessage(string.Format("Service {0} exists on {1}.", this.ServiceName, this.MachineName));
            }
            else
            {
                this.ServiceExists = false;
                Log.LogMessage(string.Format("Service {0} does not exist on {1}.", this.ServiceName, this.MachineName));
            }
        }

        /// <summary>
        /// Sets the type of the startup.
        /// </summary>
        /// <param name="startup">The startup.</param>
        private void SetStartupType(string startup)
        {
            Log.LogMessage(string.Format("Setting StartUp Type to {0} for {1}.", startup, this.ServiceName));

            // construct the management path
            ManagementPath p = new ManagementPath("Win32_Service.Name='" + this.ServiceName + "'");

            // construct the management object
            ManagementObject managementObject = new ManagementObject(p);

            // we will use the invokeMethod method of the ManagementObject class
            object[] parameters = new object[1];
            parameters[0] = startup;
            managementObject.InvokeMethod("ChangeStartMode", parameters);
        }

        /// <summary>
        /// Starts Service.
        /// </summary>
        private void StartService()
        {
            using (ServiceController serviceController = new ServiceController(this.ServiceName, this.MachineName))
            {
                // We use this variable to count how many times we have attempted the action.
                int i = 1;
                bool actionCompleted = false;

                while (actionCompleted == false)
                {
                    // Get the updated status.
                    serviceController.Refresh();

                    switch (serviceController.Status)
                    {
                        // We can't do anything when Service is in these states, so we log, count, pause and loop.
                        case ServiceControllerStatus.ContinuePending:
                        case ServiceControllerStatus.PausePending:
                        case ServiceControllerStatus.StartPending:
                        case ServiceControllerStatus.StopPending:
                            Log.LogMessage(string.Format("Service is in an unsupported state: {0} - {1}. Looping...", this.ServiceName, serviceController.Status));
                            ++i;
                            break;
                        case ServiceControllerStatus.Paused:
                        case ServiceControllerStatus.Stopped:
                            Log.LogMessage(string.Format("Service is Paused or Stopped, attempting to start: {0}", this.ServiceName));
                            serviceController.Start();
                            ++i;
                            break;
                        case ServiceControllerStatus.Running:
                            Log.LogMessage(string.Format("Service has Started: {0}", this.ServiceName));
                            actionCompleted = true;
                            break;
                    }

                    // If we have tried for too long then we give up at this stage.
                    if ((i > this.maxCount) && (actionCompleted == false))
                    {
                        Log.LogError(string.Format("Could not start Service: {0}", this.ServiceName));
                        return;
                    }

                    // No point in pausing if the action has completed and we are just exiting the loop.
                    if (actionCompleted == false)
                    {
                        // We wait a while before re-checking Status.
                        Thread.Sleep(this.pauseMilliSeconds);
                    }
                }
            }

            return;
        }

        /// <summary>
        /// Uninstalls Service.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        private void UninstallService()
        {
            // check to see if the exe path has been provided
            if (string.IsNullOrEmpty(this.ServiceExePath))
            {
                Log.LogError("ServiceExePath was not provided.");
                return;
            }

            // check to see if the correct path has been provided
            if (File.Exists(this.ServiceExePath) == false)
            {
                Log.LogError(string.Format("ServiceExePath does not exist: {0}", this.ServiceExePath));
                return;
            }

            if (this.StopService())
            {
                Process proc = new Process();
                proc.StartInfo.FileName = GetInstallUtilPath();

                proc.StartInfo.UseShellExecute = false;
                proc.StartInfo.RedirectStandardOutput = true;
                proc.StartInfo.RedirectStandardError = true;
                proc.StartInfo.RedirectStandardOutput = true;
                proc.StartInfo.Arguments = string.Format(CultureInfo.InvariantCulture, @"/u ""{0}"" /LogFile=""{1}""", this.ServiceExePath, this.ServiceName + " Uninstall.txt");

                // start the process
                Log.LogMessage(MessageImportance.High, "Running " + proc.StartInfo.FileName + " " + proc.StartInfo.Arguments);
                proc.Start();
                string outputStream = proc.StandardOutput.ReadToEnd();
                if (outputStream.Length > 0)
                {
                    Log.LogMessage(outputStream);
                }

                string errorStream = proc.StandardError.ReadToEnd();
                if (errorStream.Length > 0)
                {
                    Log.LogError(errorStream);
                }

                proc.WaitForExit();
                if (proc.ExitCode != 0)
                {
                    Log.LogError("Non-zero exit code from InstallUtil: " + proc.ExitCode);
                    return;
                }
            }
        }

        /// <summary>
        /// Stops Service.
        /// </summary>
        /// <returns>a boolean value</returns>
        private bool StopService()
        {
            using (ServiceController serviceController = new ServiceController(this.ServiceName, this.MachineName))
            {
                // We use this variable to count how many times we have attempted the action.
                int i = 1;
                bool actionCompleted = false;

                while (actionCompleted == false)
                {
                    // Get the updated status.
                    serviceController.Refresh();

                    switch (serviceController.Status)
                    {
                        // We can't do anything when Service is in these states, so we log, count, pause and loop.
                        case ServiceControllerStatus.ContinuePending:
                        case ServiceControllerStatus.PausePending:
                        case ServiceControllerStatus.StartPending:
                        case ServiceControllerStatus.StopPending:
                            Log.LogMessage(string.Format("Service is in an unsupported state: {0} - {1}. Looping...", this.ServiceName, serviceController.Status));
                            ++i;
                            break;
                        case ServiceControllerStatus.Paused:
                        case ServiceControllerStatus.Running:
                            Log.LogMessage(string.Format("Service is Running, attempting to stop: {0}", this.ServiceName));
                            serviceController.Stop();
                            ++i;
                            break;
                        case ServiceControllerStatus.Stopped:
                            Log.LogMessage(string.Format("Service is Stopped: {0}", this.ServiceName));
                            actionCompleted = true;
                            break;
                    }

                    // If we have tried for too long then we give up at this stage.
                    if ((i > this.maxCount) && (actionCompleted == false))
                    {
                        Log.LogError(string.Format("Could not stop Service: {0}", this.ServiceName));
                        return false;
                    }

                    // No point in pausing if the action has completed and we are just exiting the loop.
                    if (actionCompleted == false)
                    {
                        // We wait a while before re-checking Status.
                        Thread.Sleep(this.pauseMilliSeconds);
                    }
                }
            }

            return true;
        }

        /// <summary>
        /// Services the exists.
        /// </summary>
        /// <returns>a boolean value</returns>
        private bool ServiceDoesExist()
        {
            bool exists = false;
            ServiceController[] allServices = ServiceController.GetServices(this.MachineName);
            foreach (ServiceController service in allServices)
            {
                if (string.Compare(service.ServiceName, this.ServiceName, StringComparison.CurrentCultureIgnoreCase) == 0)
                {
                    exists = true;
                    break;
                }
            }

            return exists;
        }

        /// <summary>
        /// Installs Service.
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
        private void InstallService()
        {
            // check to see if the exe path has been provided
            if (string.IsNullOrEmpty(this.ServiceExePath))
            {
                Log.LogError("ServiceExePath was not provided.");
                return;
            }

            // check to see if the correct path has been provided
            if (File.Exists(this.ServiceExePath) == false)
            {
                Log.LogError(string.Format("ServiceExePath does not exist: {0}", this.ServiceExePath));
                return;
            }

            using (Process proc = new Process())
            {
                proc.StartInfo.FileName = GetInstallUtilPath();
                proc.StartInfo.UseShellExecute = false;
                proc.StartInfo.RedirectStandardOutput = true;
                proc.StartInfo.RedirectStandardError = true;
                proc.StartInfo.RedirectStandardOutput = true;
                proc.StartInfo.Arguments = string.Format(CultureInfo.InvariantCulture, @"""{0}""", this.ServiceExePath);
                Log.LogMessage(MessageImportance.High, "Running " + proc.StartInfo.FileName + " " + proc.StartInfo.Arguments);
                proc.Start();

                proc.WaitForExit();
                if (proc.ExitCode != 0)
                {
                    Log.LogError("Non-zero exit code from InstallUtil.exe: " + proc.ExitCode);
                    return;
                }
            }

            using (Process proc = new Process())
            {
                proc.StartInfo.FileName = "sc.exe";

                proc.StartInfo.UseShellExecute = false;
                proc.StartInfo.RedirectStandardOutput = true;
                proc.StartInfo.RedirectStandardError = true;
                proc.StartInfo.RedirectStandardOutput = true;
                proc.StartInfo.Arguments = string.Format(CultureInfo.InvariantCulture, @"config ""{0}"" obj= ""{1}"" password= ""{2}""", this.serviceName, this.User, this.Password);

                // start the process
                Log.LogMessage(MessageImportance.High, "Running " + proc.StartInfo.FileName + " " + proc.StartInfo.Arguments.Replace(this.Password, "***CENSORED***"));
                proc.Start();
                string outputStream = proc.StandardOutput.ReadToEnd();
                if (outputStream.Length > 0)
                {
                    Log.LogMessage(outputStream);
                }

                string errorStream = proc.StandardError.ReadToEnd();
                if (errorStream.Length > 0)
                {
                    Log.LogError(errorStream);
                }

                proc.WaitForExit();
                if (proc.ExitCode != 0)
                {
                    Log.LogError("Non-zero exit code from sc.exe: " + proc.ExitCode);
                    return;
                }
            }
        }
    }
}
